﻿#region Using

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using XNgine.Exceptions;
using XNgine.Services;

#endregion

namespace XNgine
{
    public delegate void FunctionCall();

    public sealed class XEngine : Game
    {
        #region Properties

        public GameTime GameTime { get; private set; }

        /// <summary>
        /// Engine instance (since engine is a singleton)
        /// </summary>
        public static XEngine Instance { get; private set; }

        public GameSettings Settings { get; set; }

        #region Service Collection Operations Region

        /// <summary>
        /// Generic wrapper around XEngine.Services.GetService
        /// </summary>
        /// <typeparam name="T">Service type</typeparam>
        /// <returns>Requested ServiceProvider or null</returns>
        public T GetService<T>() where T : IService
        {
            T result = (T)Services.GetService(typeof(T));
            if (Equals(result, default(T))) throw new ServiceNotRegisteredException(typeof(T));
            return result;
        }

        /// <summary>
        /// Generic wrapper around XEngine.Services.AddService
        /// </summary>
        /// <typeparam name="T">Type of the ServiceProvider</typeparam>
        /// <param name="type">Service type</param>
        /// <param name="provider">Service provider</param>
        public void AddService<T>(Type type, T provider) where T : IService
        {
            Services.AddService(type, provider);
        }

        /// <summary>
        /// Generic wrapper around XEngine.Services.AddService
        /// </summary>
        /// <typeparam name="T">Type of the ServiceProvider</typeparam>
        /// <param name="provider">Service provider</param>
        public void AddService<T>(T provider) where T : IService
        {
            Services.AddService(typeof(T), provider);
        }

        /// <summary>
        /// Wrapper around XEngine.Services.RemoveService
        /// </summary>
        /// <typeparam name="T">Service type to remove</typeparam>
        public void RemoveService<T>() where T : IService
        {
            Services.RemoveService(typeof(T));
        }

        #endregion

        private GraphicsDeviceManager _graphicsDeviceManager;

        public GraphicsDeviceManager GraphicsDeviceManager
        {
            get { return _graphicsDeviceManager; }
        }

        public SpriteBatch SpriteBatch { get; private set; }

        #endregion

        #region Private attributes

        private bool _applyDeviceChanges;

        #endregion

        #region Initialization

        /// <summary>
        /// This function will be called at the end of CreateEngine.
        /// You should use it to setup all the components, managers and services required by the game.
        /// </summary>
        public static FunctionCall EngineSetup;

        public static void CreateEngine()
        {
            if (Instance != null)
                throw new Exception("Engine cannot be created more than once!");
            Instance = new XEngine();
        }

        public XEngine()
        {
            Settings = new GameSettings();
            Content = new ContentManager(Services) { RootDirectory = "Content" };
            SetupGraphicsDevice();
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            GraphicsDeviceManagers_DeviceReset(null, EventArgs.Empty);
            SpriteBatch = new SpriteBatch(this.GraphicsDevice);

            InitializeDebugConsole();

            if (EngineSetup == null) 
            {
                throw new Exception("You must define EngineSetup method!");
            }
            EngineSetup();

            GameTime = new GameTime();

            base.Initialize();
        }

        private void InitializeDebugConsole()
        {
            DebugConsole.Console console = new DebugConsole.Console { UpdateOrder = 1000, DrawOrder = 1000 };

            AddService<IConsole>(console);
            this.Components.Add(console);
            console.Enabled = false;
        }

        private void SetupGraphicsDevice()
        {
            _graphicsDeviceManager = new GraphicsDeviceManager(this)
                                         {
                                             MinimumPixelShaderProfile = ShaderProfile.PS_2_0,
                                             MinimumVertexShaderProfile = ShaderProfile.VS_2_0,
                                             SynchronizeWithVerticalRetrace = Settings.VSync
                                         };

            // Demand to update as fast as possible, do not use fixed time steps.
            this.IsFixedTimeStep = false;

            _graphicsDeviceManager.PreparingDeviceSettings += _graphicsDeviceManager_PreparingDeviceSettings;
            _graphicsDeviceManager.DeviceReset += GraphicsDeviceManagers_DeviceReset;

            ApplyResoulutionChange();
        }

        private void _graphicsDeviceManager_PreparingDeviceSettings(object sender, PreparingDeviceSettingsEventArgs e)
        {
            PresentationParameters presentParams = e.GraphicsDeviceInformation.PresentationParameters;

            presentParams.MultiSampleType = Settings.MultiSample;

            // Add support for NVidia PerfHUD.
            foreach (GraphicsAdapter currentAdapter in GraphicsAdapter.Adapters)
            {
                if (currentAdapter.Description.Contains("Perf"))
                {
                    e.GraphicsDeviceInformation.Adapter = currentAdapter;
                    e.GraphicsDeviceInformation.DeviceType = DeviceType.Reference;
                    break;
                }
            }
        }

        private void GraphicsDeviceManagers_DeviceReset(object sender, EventArgs e)
        {
            // Update width and height
            Settings.ResolutionWidth = _graphicsDeviceManager.GraphicsDevice.Viewport.Width;
            Settings.ResolutionHeight = _graphicsDeviceManager.GraphicsDevice.Viewport.Height;
            Settings.AspectRatio = (float)Settings.ResolutionWidth / Settings.ResolutionHeight;

            Settings.Save();
        }

        #endregion

        #region Private helpers

        private void ApplyResoulutionChange()
        {
            int resolutionWidth = Settings.ResolutionWidth;
            int resolutionHeight = Settings.ResolutionHeight;

            if (resolutionWidth <= 0 || resolutionWidth <= 0)
            {
                resolutionWidth = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width;
                resolutionHeight = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height;
            }

            _graphicsDeviceManager.PreferredBackBufferWidth = resolutionWidth;
            _graphicsDeviceManager.PreferredBackBufferHeight = resolutionHeight;
            _graphicsDeviceManager.IsFullScreen = Settings.Fullscreen;

            _applyDeviceChanges = true;
        }

        #endregion

        #region Main loop

        /// <summary>
        /// The main loop of the engine
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            base.Update(gameTime);
            GameTime = gameTime;
            // Apply device changes
            if (_applyDeviceChanges)
            {
                _graphicsDeviceManager.ApplyChanges();
                _applyDeviceChanges = false;
            }
        }

        #endregion

        #region Content Unload

        /// <summary>  
        /// UnloadContent will be called once per game and is the place to unload  
        /// all content.  
        /// </summary>  
        protected override void UnloadContent()
        {
            base.UnloadContent();
            Content.Unload();
        }

        #endregion
    }
}